Conversation
|
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
…extprotocol/typescript-sdk into middleware-remove-console-warn
|
@claude review |
There was a problem hiding this comment.
The previously flagged integration test bug has been fixed. The implementation looks correct, but this PR adds a new option to bypass DNS rebinding protection, so a human should sign off on the API design.
Extended reasoning...
Overview
This PR modifies the Express and Hono middleware packages to (1) remove console.warn log pollution when binding to 0.0.0.0/:: without allowedHosts, and (2) add a new skipHostHeaderValidation option using a discriminated union type that makes allowedHosts and skipHostHeaderValidation: true mutually exclusive at the type level. Five files are changed across both middleware packages and integration tests.
Security risks
The core change introduces an explicit opt-in to disable DNS rebinding protection entirely via skipHostHeaderValidation: true. While the implementation is correct (requires explicit opt-in, default behavior unchanged, clean type constraints), this is a new security-relevant API surface. A human reviewer should validate that this escape hatch is appropriate and that the documentation/naming adequately communicates the security implications.
Level of scrutiny
This warrants careful human review. Although the code is well-structured and the discriminated union type prevents misconfiguration at compile time, the PR touches security middleware (host header validation / DNS rebinding protection). The interface-to-type change is also technically a minor breaking change for anyone extending the options interface. A missing changeset was also flagged by the changeset bot.
Other factors
The author addressed the integration test bug I previously flagged — the old console.warn assertion tests are now replaced with proper HTTP-level supertest assertions. The nit about Express unit tests only checking toBeDefined() is mitigated by the new integration tests which do verify HTTP behavior with supertest. Test coverage is now solid across both packages.
| test('should not apply host validation for 0.0.0.0 without allowedHosts', async () => { | ||
| const app = createMcpExpressApp({ host: '0.0.0.0' }); | ||
| app.post('/test', (_req: Request, res: Response) => { | ||
| res.json({ success: true }); | ||
| }); | ||
|
|
||
| // No host validation applied, so any host should be accepted | ||
| const response = await supertest(app).post('/test').set('Host', 'anything.com:3000').send({}); | ||
| expect(response.status).toBe(200); | ||
| }); |
There was a problem hiding this comment.
🟡 Nit: The new test at line 2311 ("should not apply host validation for 0.0.0.0 without allowedHosts") is functionally identical to the pre-existing test at line 2262 ("should not apply host validation when host is 0.0.0.0") — both create an app with host: '0.0.0.0', POST with an arbitrary Host header, and assert 200. This should instead cover the :: (IPv6 all-interfaces) case, which lost its test coverage when the old console.warn test was removed.
Extended reasoning...
The Duplicate
The test at line 2262 ("should not apply host validation when host is 0.0.0.0") and the new test at line 2311 ("should not apply host validation for 0.0.0.0 without allowedHosts") are functionally identical:
- Both call
createMcpExpressApp({ host: '0.0.0.0' }) - Both add a POST
/testhandler that returns{ success: true } - Both send a request with an arbitrary Host header (
any-host.com:3000vsanything.com:3000) - Both assert
response.statusis 200
The only differences are the test name, the specific host header string used, and the older test also asserts response.body.
How This Happened
Before this PR, there were two console.warn tests — one for 0.0.0.0 (line 2311 in the old code) and one for :: (line 2318 in the old code). The PR replaced both with behavioral tests, but the replacement for the 0.0.0.0 warn test duplicates the pre-existing behavioral test at line 2262 instead of testing something new.
Step-by-Step Proof
- Line 2262:
createMcpExpressApp({ host: '0.0.0.0' })→ POST/testwithHost: any-host.com:3000→ assert 200 - Line 2311:
createMcpExpressApp({ host: '0.0.0.0' })→ POST/testwithHost: anything.com:3000→ assert 200 - These exercise the exact same code path:
hostis'0.0.0.0', which is not inlocalhostHosts, so no validation middleware is applied, and any Host header is accepted.
Lost Coverage
The old test "should warn when binding to :: (IPv6 all interfaces)" was the only test exercising the :: host case. When it was removed, no replacement test for host: '::' was added. The :: case follows the same code path as 0.0.0.0 (not in localhostHosts, no validation applied), but it still deserves its own test to confirm IPv6 all-interfaces behavior.
Impact
This is a minor issue — both tests pass fine and no incorrect behavior results. It is test redundancy and a missed opportunity to cover the :: case.
Suggested Fix
Change the test at line 2311 to use host: '::' instead of host: '0.0.0.0', making it cover the IPv6 all-interfaces case that lost its test.
Remove console.warn log pollution and add
skipHostHeaderValidationoptionMotivation and Context
Fixes #1515. The middleware packages (
@modelcontextprotocol/express,@modelcontextprotocol/hono) emit aconsole.warnwhen binding to0.0.0.0or::withoutallowedHosts. This pollutes server logs for users who intentionally bind to all interfaces (e.g., behind a reverse proxy in a container). Libraries should not produce unsolicited log output in environments they don't own.Additionally, there was no way to opt out of the automatic localhost host-header validation — useful when running behind a reverse proxy that rewrites the
Hostheader.This PR:
console.warnentirely. TheallowedHostsoption is already well-documented in JSDoc.skipHostHeaderValidation— an explicit opt-out from all automatic host-header validation middleware.HostHeaderValidationOptions) so thatallowedHostsandskipHostHeaderValidation: trueare mutually exclusive at the type level — passing both is a compile error, not a silent precedence rule.How Has This Been Tested?
@modelcontextprotocol/expressand@modelcontextprotocol/honoskipHostHeaderValidation: trueon both localhost and0.0.0.0hostsHostheaders succeed)Breaking Changes
console.warnwhen binding to0.0.0.0/::withoutallowedHostsis removed. This is a behavior change but not a breaking API change — no user code depended on this warning.CreateMcpExpressAppOptionsandCreateMcpHonoAppOptionschanged frominterfacetotype(intersection withHostHeaderValidationOptions). This is source-compatible for all usage patterns exceptextends/implementson the interface, which is unlikely for options objects.Types of changes
Checklist
Additional context
The
HostHeaderValidationOptionsdiscriminated union type is defined independently in both packages (no shared import) to avoid coupling the middleware packages. The pattern is identical:This ensures that
allowedHostsis only accepted when validation is enabled, catching misconfiguration at compile time rather than silently ignoring one option.